#include "stdafx.h"
#include "ClientHandler.h"
#include "ServerSystem.h"

#include "Core\ClientConnection.h"
#include "Core\XMLConnection.h"
#include "Core\CoreXMLHandler.h"
#include "Core\Packages.h"
#include "Core\Logger.h"
#include "Core\SystemVersion.h"

#include "Poco\File.h"

#include <assert.h>

namespace ExamSystem
{
namespace Server
{
    unsigned int CClientServerHandler::ms_clientServerId = 1;
    unsigned int CClientHandler::ms_clientId = 1;
    /**************************************************************************\
     Component:     CClientServerHandler
     Description:
        This class manages a connection to the connected servers. It 
        has a FSM to record the states.

        As this class manages only one connection, so the server should create
        an instance for each connection.
    \**************************************************************************/

    CClientServerHandler::CClientServerHandler(
        CServerSystem *serverSystem,
        const SharedPtr<CXMLConnection> &connection, 
        int localID
        )
        : CSystemHandler(serverSystem, connection)
        , m_nRetrySendingDatabase(0)
        , m_localID(localID)
    {
        RegisterPackageTypes();
    }

    CClientServerHandler::~CClientServerHandler(void)
    {

    }

    void CClientServerHandler::StartHandler(void)
    {         
        m_presentState = stateVersionCheck;
    }

    void CClientServerHandler::RegisterPackageTypes(void)
    {
        m_packageHandlers[CResultPackage::packageType] = &CClientServerHandler::ResultPackageHandler;
        m_packageHandlers[CVersionPackage::packageType] = &CClientServerHandler::VersionPackageHandler;
        m_packageHandlers[CServerInfoPackage::packageType] = &CClientServerHandler::ServerInfoPackageHandler;
        m_packageHandlers[CConnectionInfoPackage::packageType] = &CClientServerHandler::ConnectionInfoPackageHandler;
        m_packageHandlers[CDatabasePackage::packageType] = &CClientServerHandler::DatabasePackageHandler;

        SharedPtr<CCoreXMLHandler> &manager = m_xmlConnection->GetXMLHandler();
        for (std::map<std::string, PackageHandler>::iterator it = m_packageHandlers.begin();
            it != m_packageHandlers.end(); it++)
            manager->RegisterStanzaHandler(it->first, this);
    }

    void CClientServerHandler::ProcessPackage(void)
    {
        CSystemHandler::ProcessPackage();  
        
        std::map<std::string, PackageHandler>::iterator it = 
            m_packageHandlers.find(m_currentPackage->GetPackageType());

        if (it == m_packageHandlers.end())
        {
            // we don't handler other packages
            systemLogger.warning("Received wrong package: " + m_currentPackage->GetPackageType());
            return;
        }
        (this->*(it->second))();
    }

    int CClientServerHandler::GetLocalID(void) const
    {
        return m_localID;
    }

    void CClientServerHandler::ResultPackageHandler(void)
    {
        switch (m_presentState)
        {
        case stateSendindDatabase:
            SendingDatabaseResultPackageHandler();
            break;
        }
    }

    void CClientServerHandler::VersionPackageHandler(void)
    {

        switch (m_presentState)
        {
        case stateVersionCheck:
            SystemVersionCheckPackageHandler();
            break;
        case stateDatabaseCheck:
            DatabaseVersionCheckPackageHandler();
            break;
        }
    }

    void CClientServerHandler::ServerInfoPackageHandler(void)
    {
        // we don't use this package any longer
        // we now use ConnectionInfoPackage
        //eventServerInfoUpdated(this, *static_cast<CServerInfoPackage *>(m_currentPackage.get()));
    }

    void CClientServerHandler::ConnectionInfoPackageHandler(void)
    {
        eventConnectionInfoUpdated(this, *static_cast<CConnectionInfoPackage *>(m_currentPackage.get()));
    }

    void CClientServerHandler::SystemVersionCheckPackageHandler(void)
    {
        SharedPtr<CPackage> result = packageManager.CreatePackage(CResultPackage::packageType);
        if (static_cast<CVersionPackage *>(m_currentPackage.get())->m_versionString == ExamSystemVersion)
        {
            // check passed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Passed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());

            if (m_currentPackage->m_sourseID == 0)  // the ID haven't initialized, so we should alloc one
            {
                CPackagePtr allocID = packageManager.CreatePackage(CIDAllocPackage::packageType);
                static_cast<CIDAllocPackage *>(allocID.get())->m_ID = ms_clientServerId++;
                allocID->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            }

            // ask server system whether we should redirect this connection
            CPackagePtr redirect = packageManager.CreatePackage(CRedirectPackage::packageType);
            if (m_serverSystem->RedirectConnection(this, static_cast<CRedirectPackage *>(redirect.get())->m_targetAddress))
            {
                static_cast<CRedirectPackage *>(redirect.get())->m_redirected = true;
                redirect->WriteToStream(m_xmlConnection->GetSocketOutputStream());
                m_presentState = stateRedirecting;
            }
            else
            {
                static_cast<CRedirectPackage *>(redirect.get())->m_redirected = false;
                redirect->WriteToStream(m_xmlConnection->GetSocketOutputStream());

                // send exam information
                CPackagePtr pkg = packageManager.CreatePackage(CInformationPackage::packageType);
                static_cast<CInformationPackage *>(pkg.get())->m_information = m_serverSystem->GetExamInformation();
                pkg->WriteToStream(m_xmlConnection->GetSocketOutputStream());
                m_presentState = stateDatabaseCheck;
            }
        }
        else
        {
            // check failed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Failed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            m_presentState = stateError;
            eventVersionCheckFailed(this, this);
        }
    }

    void CClientServerHandler::DatabaseVersionCheckPackageHandler(void)
    {
        SharedPtr<CPackage> result = packageManager.CreatePackage(CResultPackage::packageType);
        if (static_cast<CVersionPackage *>(m_currentPackage.get())->m_versionString == m_serverSystem->GetQuestionLibraryVersion())
        {
            // check passed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Passed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            m_presentState = stateReady;
            eventReady(this, this);
        }
        else
        {
            // check failed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Failed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            m_presentState = stateSendindDatabase;

            // send the database
            SharedPtr<CPackage> database = packageManager.CreatePackage(CDatabasePackage::packageType);
            static_cast<CDatabasePackage *>(database.get())->SetDatabaseFile(m_serverSystem->GetQuestionLibraryPath());
            database->WriteToStream(m_xmlConnection->GetSocketOutputStream());
        }
    }

    void CClientServerHandler::SendingDatabaseResultPackageHandler(void)
    {
        if (static_cast<CResultPackage *>(m_currentPackage.get())->m_result == CResultPackage::Passed)
        {
            // succeeded
            m_presentState = stateReady;
            eventReady(this, this);
        }
        else
        {
            // failed, try to resend
            if (m_nRetrySendingDatabase < maxRetryTime)
            {              
                // send the database
                SharedPtr<CPackage> database = packageManager.CreatePackage(CDatabasePackage::packageType);
                static_cast<CDatabasePackage *>(database.get())->SetDatabaseFile(m_serverSystem->GetQuestionLibraryPath());
                database->WriteToStream(m_xmlConnection->GetSocketOutputStream());

                m_nRetrySendingDatabase++;
                systemLogger.warning("Failed to send database file, retrying...");
            }
            else
            {
                m_presentState = stateError;
                systemLogger.error("Failed to send database file.");
                eventFailedToSendDatabase(this, this);
            }
        }
    }

    void CClientServerHandler::DatabasePackageHandler(void)
    {
        CDatabasePackage &database = StaticCast<CDatabasePackage>(m_currentPackage);
        CPackagePtr pkg = packageManager.CreatePackage(CResultPackage::packageType);
        CResultPackage &result = StaticCast<CResultPackage>(pkg);

        if (database.IsSucceeded())
        {
            if (m_serverSystem->MergeAnswerDatabase(database.GetFileName()))
            {
                Poco::File file(database.GetFileName());
                try {
                    file.remove();
                } catch (...) {}

            }
            else
            {
                // if we failed to merge the database, there might be some internal error
                systemLogger.fatal("Failed to merge answer database: "
                    + database.GetFileName() + ". The file was reserved.");
            }
            result.m_result = CResultPackage::Passed;
        }
        else
        {
            result.m_result = CResultPackage::Failed;
        }
        result.WriteToStream(m_xmlConnection->GetSocketOutputStream());
    }


    /**************************************************************************\
     Component:     CClientHandler
     Description:
        This class manages a connection to the connected clients. It 
        has a FSM to record the states.
     \**************************************************************************/

    CClientHandler::CClientHandler(
        CServerSystem *serverSystem, 
        const SharedPtr<CXMLConnection> &connection,
        int localID
        )
        : CSystemHandler(serverSystem, connection)
        , m_localID(localID)
    {
        RegisterPackageTypes();
    }

    CClientHandler::~CClientHandler(void)
    {

    }

    void CClientHandler::StartHandler(void)
    {
        m_presentState = stateVersionCheck;
    }


    void CClientHandler::RegisterPackageTypes(void)
    {
        m_packageHandlers[CResultPackage::packageType] = &CClientHandler::ResultPackageHandler;
        m_packageHandlers[CVersionPackage::packageType] = &CClientHandler::VersionPackageHandler;
        m_packageHandlers[CLoginPackage::packageType] = &CClientHandler::LoginPackageHandler;
        m_packageHandlers[CConnectionInfoPackage::packageType] = &CClientHandler::ConnectionInfoPackageHandler;
        m_packageHandlers[CQuestionQueryPackage::packageType] = &CClientHandler::QuestionQueryPackageHandler;
        m_packageHandlers[CCriterionQueryPackage::packageType] = &CClientHandler::CriterionQueryPackageHandler;
        m_packageHandlers[CDatabasePackage::packageType] = &CClientHandler::DatabasePackageHandler;

        SharedPtr<CCoreXMLHandler> &manager = m_xmlConnection->GetXMLHandler();
        for (std::map<std::string, PackageHandler>::iterator it = m_packageHandlers.begin();
            it != m_packageHandlers.end(); it++)
            manager->RegisterStanzaHandler(it->first, this);
    }

    void CClientHandler::ProcessPackage(void)
    {
        CSystemHandler::ProcessPackage();

        std::map<std::string, PackageHandler>::iterator it = 
            m_packageHandlers.find(m_currentPackage->GetPackageType());

        if (it == m_packageHandlers.end())
        {
            // we don't handler other packages
            systemLogger.warning("Received wrong package: " + m_currentPackage->GetPackageType());
            return;
        }
        (this->*(it->second))();
    }

    int CClientHandler::GetLocalID(void) const
    {
        return m_localID;
    }

    void CClientHandler::ResultPackageHandler(void)
    {
        CResultPackage &result = StaticCast<CResultPackage>(m_currentPackage);
        switch (m_presentState)
        {
        case stateSendingQuestions:
        case stateSendingCriterions:
            SendNextQuestionCriterion(result.m_result != CResultPackage::Passed);
            break;
        }
    }

    void CClientHandler::VersionPackageHandler(void)
    {
        SharedPtr<CPackage> result = packageManager.CreatePackage(CResultPackage::packageType);
        if (static_cast<CVersionPackage *>(m_currentPackage.get())->m_versionString == ExamSystemVersion)
        {
            // check passed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Passed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());  

            if (m_currentPackage->m_sourseID == 0)  // the ID haven't initialized, so we should alloc one
            {
                CPackagePtr allocID = packageManager.CreatePackage(CIDAllocPackage::packageType);
                static_cast<CIDAllocPackage *>(allocID.get())->m_ID = ms_clientId++;
                allocID->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            }

            // ask server system whether we should redirect this connection
            CPackagePtr redirect = packageManager.CreatePackage(CRedirectPackage::packageType);
            if (m_serverSystem->RedirectConnection(this, static_cast<CRedirectPackage *>(redirect.get())->m_targetAddress))
            {
                static_cast<CRedirectPackage *>(redirect.get())->m_redirected = true;
                redirect->WriteToStream(m_xmlConnection->GetSocketOutputStream());
                m_presentState = stateRedirecting;
            }
            else
            {
                static_cast<CRedirectPackage *>(redirect.get())->m_redirected = false;
                redirect->WriteToStream(m_xmlConnection->GetSocketOutputStream());

                // send exam information
                CPackagePtr pkg = packageManager.CreatePackage(CInformationPackage::packageType);
                static_cast<CInformationPackage *>(pkg.get())->m_information = m_serverSystem->GetExamInformation();
                pkg->WriteToStream(m_xmlConnection->GetSocketOutputStream());

                m_presentState = stateWaitForLogin;
            }
        }
        else
        {
            // check failed
            static_cast<CResultPackage *>(result.get())->m_result = CResultPackage::Failed;
            result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
            m_presentState = stateError;
            eventVersionCheckFailed(this, this);
        }
    }

    void CClientHandler::LoginPackageHandler(void)
    {
        switch (m_presentState)
        {
        case stateWaitForLogin:
            StudentLoginHandler();
            break;
        case stateWaitForInvigilator:
            InvigilatorLoginHandler();
            break;
        }
    }

    void CClientHandler::ConnectionInfoPackageHandler(void)
    {
        eventConnectionInfoUpdated(this, *static_cast<CConnectionInfoPackage *>(m_currentPackage.get()));
    }

    void CClientHandler::StudentLoginHandler(void)
    {
        CLoginPackage *pkg = static_cast<CLoginPackage *>(m_currentPackage.get());
        CPackagePtr result = packageManager.CreatePackage(CLoginResultPackage::packageType);
        switch (m_serverSystem->StudentLogin(this, pkg->m_username, pkg->m_password))
        {
        case CLoginHelper::Succeeded:
            if (m_serverSystem->IsExamStart())
            {
                static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::NeedInvigilator;
                m_presentState = stateWaitForInvigilator;
            }
            else
            {
                static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Succeeded;
                m_presentState = stateReady;
                eventReady(this, this);
            }
            break;
        case CLoginHelper::Failed:
            static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Failed;
            break;
            //case CServerSystem::Error:
        default:
            static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Error;
            break;
        }
        result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
    }

    void CClientHandler::InvigilatorLoginHandler(void)
    {
        CLoginPackage *pkg = static_cast<CLoginPackage *>(m_currentPackage.get());
        CPackagePtr result = packageManager.CreatePackage(CLoginResultPackage::packageType);
        switch (m_serverSystem->InvigilatorLogin(this, pkg->m_username, pkg->m_password))
        {
        case CLoginHelper::Succeeded:
            static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Succeeded;
            m_presentState = stateReady;
            eventReady(this, this);
            break;
        case CLoginHelper::Failed:
            static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Failed;
            break;
            //case CServerSystem::Error:
        default:
            static_cast<CLoginResultPackage *>(result.get())->m_result = CLoginResultPackage::Error;
            break;
        }
        result->WriteToStream(m_xmlConnection->GetSocketOutputStream());
    }

    void CClientHandler::PrepareQuestions(void)
    {
        m_serverSystem->GenerateQuestions(m_questionIds);
        m_currentSendingType = 0;
        m_currentSendingIdx = -1;
        m_presentState = stateSendingQuestions;
    }

    void CClientHandler::PrepareCriterions(void)
    {
        m_currentSendingType = 0;
        m_currentSendingIdx = -1;
        m_presentState = stateSendingCriterions;
    }

    void CClientHandler::SendNextQuestionCriterion(bool flagResend)
    {
        if (m_presentState != stateSendingCriterions &&
            m_presentState != stateSendingQuestions)
            return;

        if (!flagResend)
        {
            if (m_currentSendingType == QuestionTypeCount)
            {
                m_presentState = stateReady;
                return;
            }

            m_currentSendingIdx++;
            while (m_currentSendingIdx == m_questionIds[m_currentSendingType].size())
            {
                m_currentSendingType++;
                m_currentSendingIdx = 0;
                if (m_currentSendingType == QuestionTypeCount)
                    break;
            }
        }

        assert(m_currentSendingIdx >= 0);
        if (m_presentState == stateSendingQuestions)
        {
            CPackagePtr pkg = packageManager.CreatePackage(CQuestionPackage::packageType);
            CQuestionPackage &question = StaticCast<CQuestionPackage>(pkg);
            if (m_currentSendingType < QuestionTypeCount)
            {
                question.m_question = QuestionCreator(static_cast<QuestionType>(m_currentSendingType));
                question.m_question->questionID = m_questionIds[m_currentSendingType][m_currentSendingIdx];
                m_serverSystem->GetQuestionLibrary().LoadQuestion(question.m_question);
            }
            //else
            //{
            //    // here we send a null question to complete the sending job
            //}
            pkg->WriteToStream(m_xmlConnection->GetSocketOutputStream());
        }
        else/* if (m_presentState = stateSendingCriterions)*/
        {
            CPackagePtr pkg = packageManager.CreatePackage(CCriterionPackage::packageType);
            CCriterionPackage &criterion = StaticCast<CCriterionPackage>(pkg);
            if (m_currentSendingType < QuestionTypeCount)
            {
                criterion.m_criterion = CriterionCreator(static_cast<QuestionType>(m_currentSendingType));
                criterion.m_criterion->questionID = m_questionIds[m_currentSendingType][m_currentSendingIdx];
                m_serverSystem->GetQuestionLibrary().LoadCriterion(criterion.m_criterion);
            }
            //else
            //{
            //    // here we send a null criterion to complete the sending job
            //}
            pkg->WriteToStream(m_xmlConnection->GetSocketOutputStream());
        }
    }

    void CClientHandler::QuestionQueryPackageHandler(void)
    {
        PrepareQuestions();
        SendNextQuestionCriterion(false);
    }

    void CClientHandler::CriterionQueryPackageHandler(void)
    {
        CCriterionQueryPackage &query = StaticCast<CCriterionQueryPackage>(m_currentPackage);
        for (int i = 0; i < QuestionTypeCount; i++)
            m_questionIds[i] = query.m_questionIds[i];

        PrepareCriterions();
        SendNextQuestionCriterion(false);
    }

    void CClientHandler::DatabasePackageHandler(void)
    {
        CDatabasePackage &database = StaticCast<CDatabasePackage>(m_currentPackage);
        CPackagePtr pkg = packageManager.CreatePackage(CResultPackage::packageType);
        CResultPackage &result = StaticCast<CResultPackage>(pkg);

        if (database.IsSucceeded())
        {
            if (m_serverSystem->MergeAnswerDatabase(database.GetFileName()))
            {
                Poco::File file(database.GetFileName());
                try {
                    file.remove();
                } catch (...) {}

            }
            else
            {
                // if we failed to merge the database, there might be some internal error
                systemLogger.fatal("Failed to merge answer database: "
                    + database.GetFileName() + ". The file was reserved.");
            }
            result.m_result = CResultPackage::Passed;
        }
        else
        {
            result.m_result = CResultPackage::Failed;
        }
        result.WriteToStream(m_xmlConnection->GetSocketOutputStream());
    }

}
}